﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Dynamic;
using System.Globalization;
using System.Reflection;
using System.Text;

namespace Xtreme.Json
{
  public class JsonParser
  {
    private readonly StringBuilder _sb = new StringBuilder();

    private int _index;
    private int _line;
    private int _column;

    private string _jsonText = String.Empty;

    public T Parse<T>(string json)
    {
      if (String.IsNullOrWhiteSpace(json))
      {
        return default(T);
      }

      Reset();
      _jsonText = json;

      return (T)ParseValue(typeof(T));
    }

    private void Reset()
    {
      _index = _line = _column = 0;
      _jsonText = "";
    }

    private dynamic ParseArray(Type type, object reference = null)
    {
      Read(); //read first [

      bool isDictionary = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>);

      dynamic list = reference;
      if (list == null)
      {
        if (isDictionary)
        {
          list = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(type.GetGenericArguments()));
        }
        else
        {
          Type genericType = typeof(List<>).MakeGenericType(type);
          list = Activator.CreateInstance(genericType);
        }
      }

      bool doRead = true;
      while (doRead)
      {
        ReadWhitespace();
        switch (Peek())
        {
          case -1:
            return ThrowParserError("Unterminated array before end of json string.");
          case ',':
            Read();
            break;
          case ']':
            Read();
            doRead = false;
            break;
          default:
            dynamic val = ParseValue(type);
            if (isDictionary)
            {
              GetAddMethod(type).Invoke(null, new object[] {list, val});
            }
            else
            {
              list.Add(val);
            }
            break;
        }
      }
      return list;
    }

    private static readonly Dictionary<Type, MethodInfo> _methods = new Dictionary<Type, MethodInfo>();
    private MethodInfo GetAddMethod(Type type)
    {
      MethodInfo method;
      if (_methods.TryGetValue(type, out method))
      {
        return method;
      }

      method = typeof(JsonDeserializer).GetMethod("Add", BindingFlags.Public | BindingFlags.Static).MakeGenericMethod(type.GetGenericArguments());
      try
      {
        _methods.Add(type, method);
      }
      catch (ArgumentException)
      {
        method = _methods[type];
      }
      return method;
    }

    private dynamic ParseObject(Type objectType, object reference = null)
    {
      Read(); //read first {
      ReadWhitespace();

      bool isValueType = objectType.IsValueType;
      dynamic instance = reference ?? (!isValueType ? Activator.CreateInstance(objectType) : new List<object>());

      IDictionary<string, object> dictionary = null;
      if (typeof(IDictionary<string, object>).IsAssignableFrom(objectType))
      {
        dictionary = (IDictionary<string, object>)instance;
      }

      bool doRead = true;
      while (doRead)
      {
        int c = Peek();
        switch (c)
        {
          case -1:
            return ThrowParserError("Unterminated object before end of json string.");
          case ',':
            Read();
            break;
          case '}':
            Read();
            doRead = false;
            break;
          case '"':
            string key = ParseString();

            ReadWhitespace();
            ReadExpect(':');
            ReadWhitespace();

            PropertyInfo info = objectType.GetProperty(key, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.IgnoreCase);
            if (info != null)
            {
              GetPropertyValue(ref instance, isValueType, info);
            }
            else
            {
              dynamic value = ParseValue(null); //Skip over the object
              if (dictionary != null)
              {
                dictionary.Add(key, value);
              }
            }
            break;
          default:
            if (char.IsWhiteSpace((char)c))
            {
              ReadWhitespace();
              break;
            }
            ThrowParserError("Invalid JSON character token encountered.");
            break;
        }
      }

      if (isValueType)
      {
        return Activator.CreateInstance(objectType, instance.ToArray());
      }
      return instance;
    }

    private void GetPropertyValue(ref dynamic instance, bool isValueType, PropertyInfo info)
    {
      Type propertyType = info.PropertyType;

      dynamic value = null;
      if (info.SetMethod == null && !isValueType)
      {
        value = info.GetMethod.Invoke(instance, null);
      }

      value = ParseValue(propertyType, value);
      if (value != null && info.SetMethod != null)
      {
        info.SetValue(instance, GetValue(propertyType, value));
      }
      else if (isValueType)
      {
        instance.Add(value);
      }
    }

    private static dynamic GetValue(Type propertyType, dynamic value)
    {
      Type valueType = value.GetType();
      if (propertyType.IsAssignableFrom(valueType))
      {
        return value;
      }
      if (propertyType.IsEnum)
      {
        return GetEnumValue(propertyType, value);
      }
      if (propertyType == typeof(DateTime))
      {
        return DateTime.Parse(value, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
      }
      TypeConverter converter = TypeDescriptor.GetConverter(propertyType);
      if (converter.CanConvertFrom(valueType))
      {
        return valueType == typeof(string) ? converter.ConvertFromInvariantString(value) : converter.ConvertFrom(value);
      }
      return Convert.ChangeType(value, propertyType, CultureInfo.InvariantCulture);
    }

    private static object GetEnumValue(Type type, dynamic value)
    {
      if (Enum.IsDefined(type, value))
      {
        if (value is string)
        {
          return Enum.Parse(type, value, true);
        }
        return Enum.ToObject(type, value);
      }

      string message = String.Format(CultureInfo.InvariantCulture, "Enum value '{0}' not found for type: {1}", value, type.FullName);
      throw new ArgumentOutOfRangeException(type.Name, value, message);
    }

    private dynamic ParseNumber(Type type)
    {
      _sb.Clear();
      TypeCode typeCode = Type.GetTypeCode(type);

      int c = Peek();
      bool doRead = true;
      do
      {
        switch (c)
        {
          case '.':
          case '-':
          case '+':
          case 'e':
          case 'E':
            _sb.Append((char)c);
            c = Read(true); //Read and Peek
            break;
          default:
            if (c >= '0' && c <= '9')
            {
              do
              {
                _sb.Append((char)c);
                c = Read(true); //Read and Peek
              } while (c >= '0' && c <= '9');
            }
            else
            {
              doRead = false;
            }
            break;
        }
      } while (doRead);

      return TryParseNumber(typeCode, _sb.ToString());
    }

    [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
    private static dynamic TryParseNumber(TypeCode typeCode, string value)
    {
      while (true)
      {
        switch (typeCode)
        {
          case TypeCode.SByte:
            {
              sbyte number;
              if (!sbyte.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
              {
                return value;
              }
              return number;
            }
          case TypeCode.Byte:
            {
              byte number;
              if (!byte.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
              {
                return value;
              }
              return number;
            }
          case TypeCode.Int16:
            {
              short number;
              if (!short.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
              {
                return value;
              }
              return number;
            }
          case TypeCode.UInt16:
            {
              ushort number;
              if (!ushort.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
              {
                return value;
              }
              return number;
            }
          case TypeCode.Int32:
            {
              int number;
              if (!int.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
              {
                return value;
              }
              return number;
            }
          case TypeCode.UInt32:
            {
              uint number;
              if (!uint.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
              {
                return value;
              }
              return number;
            }
          case TypeCode.Int64:
            {
              long number;
              if (!long.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
              {
                return value;
              }
              return number;
            }
          case TypeCode.UInt64:
            {
              ulong number;
              if (!ulong.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
              {
                return value;
              }
              return number;
            }
          case TypeCode.Single:
            {
              float number;
              if (!float.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
              {
                return value;
              }
              return number;
            }
          case TypeCode.Double:
            {
              double number;
              if (!double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
              {
                return value;
              }
              return number;
            }
          case TypeCode.Decimal:
            {
              decimal number;
              if (!decimal.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out number))
              {
                return value;
              }
              return number;
            }
          case TypeCode.Object:
            {
              typeCode = value.Contains(".") ? TypeCode.Double : TypeCode.Int64;
              continue;
            }
        }
        return value;
      }
    }

    private string ParseString()
    {
      Read(); //read first "
      _sb.Clear();

      bool complete = false;
      while (!complete)
      {
        int c = Read();
        switch (c)
        {
          case -1:
            return ThrowParserError("Unterminated string before end of json string.");
          case '"':
            complete = true;
            break;
          case '\\':
            int nc = Read();
            switch (nc)
            {
              case '"':
              case '\\':
              case '/':
                _sb.Append((char)nc);
                break;
              case 'b':
                _sb.Append('\b');
                break;
              case 'f':
                _sb.Append('\f');
                break;
              case 'n':
                _sb.Append('\n');
                break;
              case 'r':
                _sb.Append('\r');
                break;
              case 't':
                _sb.Append('\t');
                break;
              case 'u':
                ushort cp = 0;
                for (int i = 0; i < 4; i++)
                {
                  if ((c = Read()) < 0)
                  {
                    ThrowParserError("Incomplete unicode.");
                  }
                  cp *= 16;
                  if ('0' <= c && c <= '9')
                  {
                    cp += (ushort)(c - '0');
                  }
                  if ('A' <= c && c <= 'F')
                  {
                    cp += (ushort)(c - 'A' + 10);
                  }
                  if ('a' <= c && c <= 'f')
                  {
                    cp += (ushort)(c - 'a' + 10);
                  }
                }
                _sb.Append((char)cp);
                break;
              default:
                _sb.Append((char)nc);
                break;
            }
            break;
          default:
            _sb.Append((char)c);
            break;
        }
      }

      return _sb.ToString();
    }

    private dynamic ParseValue(Type type, object reference = null)
    {
      ReadWhitespace();

      Type valueType = (type ?? typeof(object)).ElementType();

      int c = Peek();
      switch (c)
      {
        case -1:
          return ThrowParserError("Unknown parsing error. Premature end of json string.");
        case '{':
          if (reference == null && valueType.FullName == "System.Object")
          {
            reference = new ExpandoObject();
            valueType = reference.GetType();
          }
          return ParseObject(valueType, reference);
        case '"':
          return GetValue(valueType, ParseString());
        case '[':
          dynamic collection = ParseArray(valueType, reference);
          if (type != null)
          {
            if (type.IsArray)
            {
              return collection.ToArray();
            }
          }
          return collection;
        case '-':
          return ParseNumber(valueType);
        case 't':
          if (TryRead("true"))
          {
            return true;
          }
          goto default;
        case 'f':
          if (TryRead("false"))
          {
            return false;
          }
          goto default;
        case 'n':
          if (TryRead("null"))
          {
            return null;
          }
          goto default;
        default:
          if (c >= '0' && c <= '9')
          {
            return ParseNumber(valueType);
          }
          return ThrowParserError("Invalid JSON character token encountered.");
      }
    }

    private int Peek()
    {
      if (_index == _jsonText.Length)
      {
        return -1; //EOF
      }
      return _jsonText[_index];
    }

    private int Read(bool andPeek = false)
    {
      if (andPeek)
      {
        ++_index;
      }
      if (_index == _jsonText.Length)
      {
        return -1;
      }
      int c = _jsonText[andPeek ? _index : _index++];
      if (c == '\n')
      {
        ++_line;
        _column = 0;
      }
      else
      {
        ++_column;
      }
      return c;
    }

    private void ReadExpect(char c)
    {
      int expect = Read();
      if (expect == -1)
      {
        ThrowParserError(String.Format(CultureInfo.InvariantCulture, "Expected '{0}' but is at the end of the string.", c));
      }
      if (expect != c)
      {
        ThrowParserError(String.Format(CultureInfo.InvariantCulture, "Expected '{0}' but received {1}.", c, expect));
      }
    }

    private void ReadWhitespace()
    {
      while (true)
      {
        switch (Peek())
        {
          case ' ':
          case '\t':
          case '\r':
          case '\n':
            Read();
            break;
          default:
            return;
        }
      }
    }

    private bool TryRead(string s)
    {
      bool success = true;
      for (int i = 0; i < s.Length; ++i)
      {
        if (s[i] != Read())
        {
          _index = _index - i - 1;
          success = false;
          break;
        }
      }
      return success;
    }

    private string ThrowParserError(string msg)
    {
      throw new JsonParserException(msg) {Line = _line, Column = _column};
    }
  }
}